加载 so
System.loadLibrary(String soName)
, libs 目录下的 so 文件会被复制到应用安装目录并完成加载System.load(String soPath)
,用于加载一个完整路径的 so 文件
注册 so
静态注册,使用
Java_{类完整路径}_{方法名}
作为 native 的方法名。当 so 已经被加载之后,native 方法在第一次被执行时候就会完成注册。1
2
3
4public class Test{
public static native String test();
}
extern "C" jstring Java_com_effective_android_test(JNIEnv *env,jclass clazz)动态注册,借助
JNI_OnLoad
方法完成绑定。当 so 被加载时会调用 JNI_OnLoad 方法进行注册。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class Test{
public static native void testJni();
}
void test(JNIEnv *env,jclass clazz){
//native impl
}
JNINativeMethod nativeMethods[] = {
{"test","()V",(void *) test}
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm,void *reserved){
//...
jclass clz = env->FindClass("com/effective/android/Test");
if(env->RegisterNatives(clz, nativeMethods,sizeOf(nativeMethods)/sizeOf(nativeMethods[0])) != JNI_OK){
return JNI_ERR;
}
//...
}
so 热替换的限制性
- 针对动态注册场景
- 对于 art 虚拟机下,可再次加载补丁 so 来完成方法映射的更新;
- 对于 dalvik 虚拟机下,需要对补丁 so 重命名来避免来完成 art 下的方法映射的更新。
- 针对静态注册场景
- 解除已经完成静态注册的方法工作难度大
- so 中哪些静态注册的方法需要更新也很难得知
上述两个场景涉及补丁 so 的二次加载,内存损耗大,可能导致 JNI OOM出现。同时如果动态注册 so 中新增了一些方法但是对应的 dex 中没有对应的代码,则会出现 NoSuchMethodError
。
so 冷启动方案
假如在在应用加载 so 之前,能够先尝试加载补丁 so,再加载应用 so,就可以实现修复。自定义一个方法,替换掉 System.loadLibrary()
来完成这个逻辑。 但是存在一个缺点就是很难修复已经混淆编译的第三方库。
这里最终采取的是类似 “类修复” 的注入方案。so 库被加载之后,最终会在 DexPathList.nativeLibararyDirectories/nativeLiraryPathElements
变量所表示的目录下遍历搜索。
SDK < 23
1 | private final File[] nativeLibraryDirectories; |
只需要把补丁 so 库的路径插到 nativeLibraryDirectories
最前面。
SDK >= 23
1 | private final File[] nativeLiraryPathElements; |
只需要为补丁 so 构建一个 element 对象并插到 nativeLiraryPathElements
最前面。
但是 so 库文件存在多种 CPU 架构,补丁和 apk 一样都存在需要选择哪个 abi 的 so 来执行的问题。
Sophix 提供了一种思路, 通过从多个 abis 目录中选择一个合适的 primaryCpuAbi
目录插到 nativeLibararyDirectories/nativeLiraryPathElements
数组中。
- SDK >= 21,直接反射拿到 ApplicationInfo 对象的
primaryCpuAbi
- SDK < 21,由于不支持 64 位所以直接把
Build.CPU_ABI, Build.CPU_ABI2
作为primaryCpuAbi
1 | static{ |
参考资料
- 从JNI_OnLoad看so的加载
- Android JNI 函数注册的两种方式(静态注册/动态注册)
- 《深入探索 Android 热修复技术原理》